#!/usr/bin/env bash

SCRIPT="${BASH_SOURCE[0]}"
DIR="$(dirname "${SCRIPT}")"

# Our macOS build machines currently run on arm64, but the rsession
# binary we produce (in the default location) is built with x86_64.
# Ensure that an x86_64 version of R is placed on the PATH first so
# that the correct R is used for tests.
if [ "$(uname)" = "Darwin" ] && [ -n "${JENKINS_URL}" ]; then
   PATH="/usr/local/bin:${PATH}"
fi

read -r -d '' USAGE <<- EOF
Run RStudio unit tests. Available flags:

--help    Show this help.
--scope   Run only tests with the given scope (core, rserver, rsession, r).
--filter  Run a subset of R tests, matching the requested filter.
EOF

section () {
    echo -e "\033[1;94m==>\033[0m \033[1;97m$1\033[0m"
}

error () {
    echo -e "\033[1;91mERROR: $1\033[0m";
    exit 1
}

checkRUnitTestFailure() {
   FAILURES=$(cat "@CMAKE_CURRENT_BINARY_DIR@/testthat-failures.log")
   if [ "${FAILURES}" != "0" ]; then
      echo "DIAGNOSTICS: checkRUnitTestFailure flagging a failure: FAILURES=${FAILURES}"
      UNIT_TEST_FAILURE=true
   fi
}

# finds libSegFault.so, and sets RSESSION_DIAGNOSTICS_LIBSEGFAULT
# as a side effect
findLibSegFault() {

   # not available on macOS
   if [ "$(uname)" = "Darwin" ]; then
      return 0
   fi

   # if already set, nothing to do
   if [ -n "${RSESSION_DIAGNOSTICS_LIBSEGFAULT}" ]; then
      return 0
   fi

   # look in common locations
   CANDIDATES=(
      /usr/lib/x86_64-linux-gnu/libSegFault.so
      /lib/x86_64-linux-gnu/libSegFault.so
      /usr/lib/i386-linux-gnu/libSegFault.so
      /lib/i386-linux-gnu/libSegFault.so
      /usr/lib/aarch64-linux-gnu/libSegFault.so
      /lib/aarch64-linux-gnu/libSegFault.so
      /lib64/libSegFault.so
      /lib/libSegFault.so
   )

   for CANDIDATE in "${CANDIDATES[@]}"; do
      if [ -e "${CANDIDATE}" ]; then
         RSESSION_DIAGNOSTICS_LIBSEGFAULT="${CANDIDATE}"
         return 0
      fi
   done

   echo "WARNING: Could not find libSegFault.so"

}

runWatchdogProcess() {

   TIMEOUTS=(gtimeout timeout)
   for TIMEOUT in "${TIMEOUTS[@]}"; do
      if command -v "${TIMEOUT}" &> /dev/null; then
         TIMEOUT_CMD="${TIMEOUT}"
         break
      fi
   done

   if [ -z "${TIMEOUT_CMD}" ]; then
      echo "WARNING: 'timeout' utility is not available; hangs will not be diagnosed"
   fi

   timeout="$1"
   shift

   privileged="$1"
   shift

   command="$1"
   shift

   local PRELOAD
   if [ "$(uname)" = "Darwin" ]; then
      PRELOAD="DYLD_INSERT_LIBRARIES=${R_LIB_DIR}/libR.dylib"
   else
      PRELOAD="LD_PRELOAD=${RSESSION_DIAGNOSTICS_LIBSEGFAULT}"
   fi

   FULL_COMMAND="env SEGFAULT_SIGNALS=\"abrt segv\" ${PRELOAD} ${command} $*; exit"

   # centos6 doesn't support the --foreground option for timeout
   if [ "${OPERATING_SYSTEM}" = "centos_6" ] || [ -z "${TIMEOUT_CMD}" ]; then
      if [ "${privileged}" = "true" ]; then
         sudo /bin/bash -c "${FULL_COMMAND}"
      else
         /bin/bash -c "${FULL_COMMAND}"
      fi
   else
      if [ "${privileged}" = "true" ]; then
         sudo "${TIMEOUT_CMD}" --foreground "${timeout}" /bin/bash -c "${FULL_COMMAND}"
      else
         "${TIMEOUT_CMD}" --foreground "${timeout}" /bin/bash -c "${FULL_COMMAND}"
      fi
   fi

   RETCODE="$?"
   if [ "${RETCODE}" = "124" ]; then
      # process is hanging - use gdb to collect a stack trace to find out why
      echo "Hang detected. Dumping backtrace..."
      PID=$(pgrep -nf "${command}")
      sudo gdb -q -batch -ex 'thread apply all backtrace' -p "${PID}"
      sudo kill -9 "${PID}"
      UNIT_TEST_FAILURE=true
   elif [ "${RETCODE}" != "0" ]; then
      echo "DIAGNOSTICS: runWatchDogProcess flagging a failure: RETCODE=${RETCODE}"
      UNIT_TEST_FAILURE=true
   fi

}

has-scope () {
   [ -z "${TEST_SCOPE}" ] || [ "${TEST_SCOPE}" = "$1" ]
}

case "$1" in

"--scope")

   if [ -z "$2" ]; then
      echo "Usage: ./rstudio-tests --scope [core|rserver|rsession|r]"
      exit 1
   fi

   TEST_SCOPE="$2"
   shift 2

;;

"--filter")

   # used for filtering to a subset of R testthat tests
   TEST_SCOPE="r"
   TEST_FILTER="$2"
   shift 2

;;

"--help")
   echo "${USAGE}"
   exit 0

esac

# try to find libSegFault if available
findLibSegFault

## On a Debug Mac IDE build via xcodebuild, the executables
## will be in a Debug folder
if [ ! -e "@CMAKE_CURRENT_BINARY_DIR@/core/rstudio-core-tests" ]; then
    RSTUDIO_CORETEST_BIN="Debug/rstudio-core-tests"
    RSTUDIO_SESSION_BIN="Debug/rsession"
else
    RSTUDIO_CORETEST_BIN="rstudio-core-tests"
    RSTUDIO_SESSION_BIN="rsession"
fi

# set R environment variables needed by rsession tests
export R_HOME=$(R RHOME)
export R_DOC_DIR=$(R --vanilla -s -e "cat(paste(R.home('doc'), sep=':'))")
export R_LIB_DIR=$(R --vanilla -s -e "cat(paste(R.home('lib'), sep=':'))")

# Hack: on RHEL9 (docker), the reported R_DOC_DIR doesn't exist which triggers
# test failures; create the folder if missing
if [ ! -e "${R_DOC_DIR}" ]; then
   sudo mkdir "${R_DOC_DIR}"
fi

UNIT_TEST_FAILURE=false

# Run core tests
if has-scope "core"; then

   section "Running 'core' tests"

   # Run only the tests that don't require root 
   runWatchdogProcess 5m false "@CMAKE_CURRENT_BINARY_DIR@/core/${RSTUDIO_CORETEST_BIN}" 
   
   # Invoke the *DropPrivTests separately because they must run as root 
   # We need separate invocations because we cannot restore privs between test cases of PermanentlyDropPrivs 
   # Skip these tests in CI for MacOS builds 
   if [ -z "${JENKINS_URL}" ] || [ "$(uname)" != "Darwin" ]; then 
      runWatchdogProcess 5m true "@CMAKE_CURRENT_BINARY_DIR@/core/${RSTUDIO_CORETEST_BIN} --gtest_filter='PosixTestsRequiresPrivilege.DISABLED_PermanentlyDropPrivUsesPrimaryGroup' --gtest_also_run_disabled_tests"
      runWatchdogProcess 5m true "@CMAKE_CURRENT_BINARY_DIR@/core/${RSTUDIO_CORETEST_BIN} --gtest_filter='PosixTestsRequiresPrivilege.DISABLED_PermanentlyDropPrivUsesAlternateGroup' --gtest_also_run_disabled_tests"
      runWatchdogProcess 5m true "@CMAKE_CURRENT_BINARY_DIR@/core/${RSTUDIO_CORETEST_BIN} --gtest_filter='PosixTestsRequiresPrivilege.DISABLED_PermanentlyDropPrivChecksGroupMembership' --gtest_also_run_disabled_tests"
   fi 

   if [ -e "@CMAKE_CURRENT_BINARY_DIR@/server_core/rstudio-server-core-tests" ]; then
      section "Running 'server_core' tests"
      runWatchdogProcess 5m false "@CMAKE_CURRENT_BINARY_DIR@/server_core/rstudio-server-core-tests"
   fi

   if [ -e "@CMAKE_CURRENT_BINARY_DIR@/shared_core/rstudio-shared-core-tests" ]; then
      section "Running 'shared_core' tests"
      runWatchdogProcess 5m false "@CMAKE_CURRENT_BINARY_DIR@/shared_core/rstudio-shared-core-tests"
   fi

fi

# Run server tests
if has-scope "rserver"; then

   if [ -e "@CMAKE_CURRENT_BINARY_DIR@/server/rserver-tests" ]; then
      section "Running 'rserver' tests..."
      cd server
      runWatchdogProcess 5m false "@CMAKE_CURRENT_BINARY_DIR@/server/rserver-tests"
      cd ..
   fi

fi

# Setup for rsession tests
if [ -e "@CMAKE_CURRENT_BINARY_DIR@/conf/rsession-dev.conf" ]; then
   SESSION_CONF_FILE="@CMAKE_CURRENT_BINARY_DIR@/conf/rsession-dev.conf"
else
   SESSION_CONF_FILE="@CMAKE_CURRENT_BINARY_DIR@/conf/rdesktop-dev.conf"
fi

if has-scope "rsession"; then

   section "Running 'rsession' tests"
   runWatchdogProcess 5m false                                    \
      "@CMAKE_CURRENT_BINARY_DIR@/session/${RSTUDIO_SESSION_BIN}" \
      --run-tests                                                 \
      --config-file="${SESSION_CONF_FILE}"

fi

if has-scope "r"; then

   section "Running 'r' tests"

   # tell testthat our source dir, binary dir
   export TESTTHAT_TESTS_DIR="@CMAKE_CURRENT_SOURCE_DIR@/tests/testthat"
   export TESTTHAT_OUTPUT_DIR="@CMAKE_CURRENT_BINARY_DIR@"

   # set TESTTHAT_FILTER if a filter was provided
   if [ -n "${TEST_FILTER}" ]; then
      TESTTHAT_FILTER="${TEST_FILTER}"
      export TESTTHAT_FILTER
   fi

   # Establish temporary folders for user state and data; this prevents the state/config on the
   # machine from affecting the tests
   RSTUDIO_FOLDER=$(mktemp -d -t rstudio-tests-XXXXXXXXXX)
   export RSTUDIO_DATA_HOME="${RSTUDIO_FOLDER}/data"
   export RSTUDIO_CONFIG_HOME="${RSTUDIO_FOLDER}/config"

   runWatchdogProcess 5m false                                                   \
      "@CMAKE_CURRENT_BINARY_DIR@/session/${RSTUDIO_SESSION_BIN}"                \
      --run-script "\"source('${TESTTHAT_TESTS_DIR}/run-tests.R'); runTests()\"" \
      --config-file="${SESSION_CONF_FILE}"

   checkRUnitTestFailure

fi

# if Pro unit tests are available, run those
PRO_TESTS="${DIR}/rstudio-pro-tests"
if [ -e "${PRO_TESTS}" ]; then
   source "${PRO_TESTS}"
fi

# return an error exit code if any unit tests failed
if [ "${UNIT_TEST_FAILURE}" = "true" ]; then
   error "One or more test suites had failures."
   exit 1
fi

# success!
exit 0
